/*
 * GestureToPoint.java
 *
 * Author: Kyle Fernald
 * Created: 2-23-2010
 * EE464K Senior Design - University of Texas
 */

package gestureacquisition;

import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.WindowAdapter;
import java.awt.image.BufferedImage;
import java.awt.image.renderable.ParameterBlock;
import java.util.ArrayList;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.Player;
import javax.media.Time;
import javax.media.control.FrameGrabbingControl;
import javax.media.format.VideoFormat;
import javax.media.util.BufferToImage;
import javax.media.jai.JAI;
import javax.media.jai.RenderedOp;
import javax.swing.JDialog;

public class GestureToPoint extends Thread{
    
    GestureAcquisitionView parent;

    //Media input variables:
    private Player player;
    private MediaLocator locator;
    private FrameGrabbingControl grabber;
    private BufferToImage btoi;

    //IMAGE PROCESSING VARIABLES:
    //The points that were detected:
    private ArrayList<Point> points;
    private ArrayList<Point> newPoints;

    private ParameterBlock extremaParam;
    private ParameterBlock thresholdParam;
    private JAI jai;
    private RenderingHints hints;
    //All the locations detected...may be some "repeat" points close together:
    private ArrayList<int[]> locations;
    private boolean running = false;
    private boolean areNewPoints = false;
    private boolean addedPoints = false;
    private boolean displayRunning = false;

    private static final int DISTANCE = 12;
    private static final int EXTREMA_PASSES = 13;
    private static final int INCREMENT = 1;
    private static final double THRESHOLD = 127;
    private static final double MAX_COLOR = 255;
    
    private static JDialog displayWindow;
    private BufferedImage image;
    private RenderedOp thresholdOp;

    public void showCamera(){
        System.out.println("Starting camera feed...");
        displayWindow = new JDialog();
        displayWindow.setVisible(true);
        Thread displayThread = new Thread(new Runnable(){
            public void run() {
                while(displayRunning){
                    if(thresholdOp != null){
                        displayWindow.getGraphics().drawImage(thresholdOp.getAsBufferedImage(), 0, 0,displayWindow.getWidth(),displayWindow.getHeight(), null);
                    }
                    GestureAcquisitionView.sleep(50);
                }
                displayWindow.dispose();
            }
        });
        displayThread.start();
        displayRunning = true;

        displayWindow.addWindowListener(new WindowAdapter(){
            public void WindowClosing(java.awt.event.WindowEvent e){
                displayRunning = false;
            }
        });
    }

    /**
     * Starts the thread. Begins when this thread's run() or start() methods are invoked.
     * Loops until the close() method is called, grabbing a picture from the camera
     * and detecting the points.
     * Closes the camera before exiting this method.
     */
    @Override
    public void run(){
        this.setPriority(Thread.MAX_PRIORITY);
        System.out.println("\tStarting Gesture to Point program.");
        running = true;
        //Should run slower than the camera's capture method at the moment:
        while(running){
            image = (BufferedImage) btoi.createImage(grabber.grabFrame());
            image.setAccelerationPriority(1);
            detectPoints();
        }
        areNewPoints = false;
        player.close();
    }

    /**
     * Returns true if there are new points available. getPoints() should not
     * be called without this returning true.
     * @return
     */
    public boolean areNewPoints(){
        return areNewPoints;
    }

    public ArrayList<Point> getPoints(){
        areNewPoints = false;
        return points;
    }

    /**
     * Detects up to eight white points in a picture and stores them to the points variable.
     * @param image the BufferedImage in which to detect the points.
     */
    private void detectPoints(){
        final int height = image.getHeight();
        int[] locationData;

        newPoints = new ArrayList<Point>(8);
        addedPoints = false;

        thresholdParam.setSource(image, 0);
        thresholdOp = jai.createNS("threshold",thresholdParam,hints);

        extremaParam.setSource(thresholdOp, 0);

        //Parse the image:
        RenderedOp extremaOp = jai.createNS("extrema",extremaParam,hints);
        //Get the locations from the BLUE color band (it's RGB, so index 2):
        locations = ((ArrayList[])extremaOp.getProperty("maxLocations"))[2];
        //Because each finger could be multiple points,
        //only keep the points that aren't too close to others:
        for(int x=0; x < locations.size(); x++){
            locationData = (int[])locations.get(x);
            if(x==0 && (image.getRGB(locationData[0], locationData[1])&0xff) <= THRESHOLD){
                return;
            }
            //Make sure to invert the points from the computer's XY plane into the normal XY plane:
            checkAddToPoints(locationData[0], height - locationData[1]);
        }
        //Only change newPoints if new ones were found, newPoints is set to false
        //when the points are read through the getPoints() method.
        if(addedPoints){
            areNewPoints = true;
            points = newPoints;
        }
    }

    /**
     * Checks if the given point is close to another already in the 'points' list.
     * Adds the point if it's not within DISTANCE from any others.
     * @param x the x-coordinate.
     * @param y the y-coordinate.
     */
    private void checkAddToPoints(int x, int y){
        Point point;
        for(int i = 0; i < newPoints.size(); i++){
            point = newPoints.get(i);
            if(Math.abs(x-point.x) <= DISTANCE && Math.abs(y-point.y) <= DISTANCE){
                return;
            }
        }
        newPoints.add(new Point(x,y));
        addedPoints = true;
    }

    //*************************
    // INITIALIZATION METHODS:
    //*************************

    /**
     * Initializes the camera and processing threads. Does not start the processing.
     * @param cameraLocation the MediaLocator string for the camera to open. (e.g: "vfw://0").
     * @return true if successful.
     */
    public boolean init(String cameraLocation){
        boolean ret = initCamera(cameraLocation);

        locations = new ArrayList<int[]>(EXTREMA_PASSES);

        //Create the parameter block used by JAI for the point-finding:
        extremaParam = new ParameterBlock();
        extremaParam.addSource(null);
        extremaParam.add(null);
        extremaParam.add(INCREMENT);
        extremaParam.add(INCREMENT);
        extremaParam.add(Boolean.TRUE);
        extremaParam.add(EXTREMA_PASSES);

        double[] low = {0,0,THRESHOLD};
        double[] high = {MAX_COLOR,MAX_COLOR,MAX_COLOR};
        double[] map = {0,0,MAX_COLOR};
        thresholdParam = new ParameterBlock();
        thresholdParam.addSource(null);
        thresholdParam.add(low);
        thresholdParam.add(high);
        thresholdParam.add(map);

        jai = new JAI();

        //The hints to use when detecting with JAI:
        hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
        hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
        hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);

        return ret;
    }

    /**
     * Initialize the camera and wait for the init to complete. This method displays its own errors.
     * @param cameraLocation the locator string for the camera instance. e.g: "vfw://0".
     * @return true if successful, false otherwise.
     */
    private boolean initCamera(String cameraLocation){
        try{
            System.out.println("\tStarting camera: "+cameraLocation+'.');
            //Set up the players and captures:
            locator = new MediaLocator(cameraLocation);
            player = Manager.createRealizedPlayer(locator);
            player.start();
            Time wait = player.getStartLatency();
            if(wait == player.LATENCY_UNKNOWN){
                wait = new Time(5.0);
            }
            //Wait for the camera to initialize:
            GestureAcquisitionView.sleep(wait.getNanoseconds()/1000000 + 1);
            //Setup the capturing object:
            grabber = (FrameGrabbingControl) player.getControl("javax.media.control.FrameGrabbingControl");
            //Set up the Buffer converter with the appropriate format:
            btoi = new BufferToImage((VideoFormat) grabber.grabFrame().getFormat());
        }
        catch(Exception e){
            GestureAcquisitionView.showErrorDialog("INITIALIZE ERROR","Failed to initialize the camera.",e);
            return false;
        }
        return true;
    }

    /**
     * Safely closes the camera and processing threads.
     */
    public void close(){
        //Tell the thread to stop and wait:
        running = false;
        while(this.isAlive()){
            GestureAcquisitionView.sleep(50);
        }
    }
}